package com.vmj.classfile;

import com.vmj.classfile.entity.*;
import com.vmj.classfile.entity.attribute.*;
import com.vmj.classfile.entity.constantinfo.*;
import com.vmj.common.Bytes;
import com.vmj.common.BytesReader;

import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;

/**
 * Created by yangshoujun on 2017/8/5 下午11:33.
 */
public class ClassReader {

    private byte[] data;
    private BytesReader bytesReader;
    private ClassFile classFile;

    public ClassReader(byte[] data) {
        this.data = data;
        this.bytesReader = new BytesReader(data, ByteOrder.BIG_ENDIAN);
    }

    public ClassFile parse() {
        ClassFile classFile = new ClassFile();
        this.classFile = classFile;
        classFile.setMagic(readFourByte());
        classFile.setMinorVersion(readTwoByte());
        classFile.setMajorVersion(readTwoByte());
        int constantPoolCount = readTwoByte();
        classFile.setConstantPoolCount(constantPoolCount);
        ConstantPool constantPool = new ConstantPool();
        classFile.setConstantPool(constantPool);
        List<ConstantInfo> constantInfos = parseConstantPool(constantPoolCount);
        constantPool.setConstantInfos(constantInfos);
        classFile.setAccessFlag(readTwoByte());
        classFile.setThisClass(readTwoByte());
        classFile.setThisClassName(constantPool.getClassName(classFile.getThisClass()));
        classFile.setSuperClass(readTwoByte());
        int superClass = classFile.getSuperClass();
        if(superClass != 0) {
            classFile.setSuperClassName(constantPool.getClassName(superClass));
        }

        int interfaceCount = readTwoByte();
        classFile.setInterfaceCount(interfaceCount);
        List<Integer> intefaces = parseInterface(interfaceCount);
        classFile.setIntefaces(intefaces);
        List<String> interfaceNames = new ArrayList<String>();
        for(int i = 0;i < interfaceCount;i++) {
            interfaceNames.add(constantPool.getClassName(intefaces.get(i)));
        }
        classFile.setInterfaceNames(interfaceNames);

        int fieldsCount = readTwoByte();
        List<MemberInfo> memberInfos = parseMemberInfo(fieldsCount);
        classFile.setFields(memberInfos);
        int methodCount = readTwoByte();
        List<MemberInfo> methods = parseMemberInfo(methodCount);
        classFile.setMethods(methods);

        List<Attribute> attributes = readAttributes(constantPool);
        classFile.setAttributes(attributes);
        return classFile;
    }

    public byte[] readByte(int count) {
        byte[] read = bytesReader.readBytes(count);
        return read;
    }

    public int readOneByte() {

        byte read = bytesReader.readByte();
        return (int)read;
    }

    public int readTwoByte() {
        return bytesReader.readUnsignedShort();
    }

    public String readMagic() {
        return readFourByte();
    }

    public String readFourByte() {

        byte[] four = bytesReader.readBytes(4);
        return Bytes.bytes2hex(four);
    }

    public int readInt() {

        int d = bytesReader.readInt();
        return d;
    }

    public double readDouble() {

        double d = bytesReader.readDouble();
        return d;
    }

    public long readLong() {

        long d = bytesReader.readLong();
        return d;
    }

    public byte[] readFourByteNative() {
        byte[] four = new byte[4];
        System.arraycopy(data, 0, four, 0,4);
        byte[] tmp = new byte[data.length - 4];
        System.arraycopy(data, 4, tmp, 0, data.length - 4);
        this.data = tmp;
        return four;
    }

    public float readFloat() {
        float d = bytesReader.readFloat();
        return d;
    }

    public List<ConstantInfo> parseConstantPool(int constantPoolCount) {
        List<ConstantInfo> constantInfos = new ArrayList<ConstantInfo>();
        constantInfos.add(null);
        for(int i = 1;i < constantPoolCount;i++) {
            int tag = readOneByte();

            ConstantInfo constantInfo = null;
            switch (tag) {
                case ConstantType.INTEGER :
                    constantInfo = new ConstantIntegerInfo(this);
                    constantInfos.add(constantInfo);
                    break;
                case ConstantType.FLOAT :
                    constantInfo = new ConstantFloatInfo(this);
                    constantInfos.add(constantInfo);
                    break;
                case ConstantType.METHOD_REF:
                    constantInfo = new ConstantMethodRefInfo(this, classFile.getConstantPool());
                    constantInfos.add(constantInfo);
                    break;
                case ConstantType.STRING:
                    constantInfo = new ConstantStringInfo(this);
                    constantInfo.setConstantPool(classFile.getConstantPool());
                    constantInfos.add(constantInfo);
                    break;
                case ConstantType.UTF_8:
                    constantInfo = new ConstantUtf8Info(this);
                    constantInfos.add(constantInfo);
                    break;
                case ConstantType.FIELD_REF:
                    constantInfo = new ConstantFieldRefInfo(this, classFile.getConstantPool());
                    constantInfos.add(constantInfo);
                    break;
                case ConstantType.CLASS:
                    constantInfo = new ConstantClassInfo(this, classFile.getConstantPool());
                    constantInfos.add(constantInfo);
                    break;
                case ConstantType.INTERFACE_METHOD_REF:
                    constantInfo = new ConstantInterfaceMethodRefInfo(this, classFile.getConstantPool());
                    constantInfos.add(constantInfo);
                    break;
                case ConstantType.LONG:
                    constantInfo = new ConstantLongInfo(this);
                    constantInfos.add(constantInfo);
                    constantInfos.add(null);
                    i++;
                    break;
                case ConstantType.NAME_AND_TYPE:
                    constantInfo = new ConstantNameAndTypeInfo(this);
                    constantInfos.add(constantInfo);
                    break;
                case ConstantType.DOUBLE:
                    constantInfo = new ConstantDoubleInfo(this);
                    constantInfos.add(constantInfo);
                    constantInfos.add(null);
                    i++;
                    break;
                case ConstantType.INVOKE_DYNAMIC:
                    constantInfo = new ConstantInvokeDynamicInfo(this);
                    constantInfos.add(constantInfo);
                    break;
                case ConstantType.METHOD_HANDLE:
                    constantInfo = new ConstantMethodHandleInfo(this);
                    constantInfos.add(constantInfo);
                    break;
                case ConstantType.METHOD_TYPE:
                    constantInfo = new ConstantMethodTypeInfo(this);
                    constantInfos.add(constantInfo);
                    break;
            }
            if(constantInfo == null) {
                System.out.println("unsupported tag: " + tag + "," + i);
                continue;
            }
            constantInfo.setTag(tag);
            constantInfo.parse();

        }

        return constantInfos;
    }

    public List<Integer> parseInterface(int count) {

        List<Integer> list = new ArrayList<Integer>();
        if(count < 1) {
            return list;
        }

        for(int i = 0;i < count;i++) {
            list.add(readTwoByte());
        }

        return list;
    }

    public List<MemberInfo> parseMemberInfo(int memberCount) {

        List<MemberInfo> mumberInfos = new ArrayList<MemberInfo>();
        if(memberCount < 1) {
            return mumberInfos;
        }

        for(int i = 0; i < memberCount;i++) {
            MemberInfo memberInfo = new MemberInfo();
            memberInfo.setAccessFlags(readTwoByte());
            int nameIndex = readTwoByte();
            memberInfo.setNameIndex(nameIndex);
            memberInfo.setName(classFile.getConstantPool().getUtf8(nameIndex));
            int descriporIndex = readTwoByte();
            memberInfo.setDescriptorIndex(descriporIndex);
            memberInfo.setAttributes(readAttributes(classFile.getConstantPool()));
            memberInfo.setDescriptor(classFile.getConstantPool().getUtf8(descriporIndex));
            mumberInfos.add(memberInfo);
        }

        return mumberInfos;
    }

    public List<Attribute> readAttributes(ConstantPool constantPool) {
        List<Attribute> attributes = new ArrayList<Attribute>();
        int attributeCount = readTwoByte();
        if(attributeCount < 1) {
            return attributes;
        }

        for(int j = 0;j < attributeCount;j++) {
            int attrNameIndex = readTwoByte();
            String attrName = classFile.getConstantPool().getUtf8(attrNameIndex);
            int attrLen = readInt();
            Attribute attribute = createAttribute(attrName, attrLen, constantPool);
            attribute.setAttributeNameIndex(attrNameIndex);
            attribute.setAttributeLength(attrLen);
            attribute.setAttributeName(attrName);
            attribute.parse();
            attributes.add(attribute);
        }

        return attributes;
    }

    private Attribute createAttribute(String attributeName, int attributeLen, ConstantPool constantPool) {
        Attribute attribute = null;
        if("Code".equals(attributeName)) {
            attribute = new CodeAttribute(this);
            attribute.setConstantPool(constantPool);
        }
        else if("ConstantValue".equals(attributeName)) {
            attribute = new ConstantValueAttribute(this);
            attribute.setConstantPool(constantPool);
        }
        else if("Deprecated".equals(attributeName)) {
            attribute = new DeprecatedAttribute(this);
            attribute.setConstantPool(constantPool);
        }
        else if("Exceptions".equals(attributeName)) {
            attribute = new ExceptionsAttribute(this);
            attribute.setConstantPool(constantPool);
        }
        else if("LineNumberTable".equals(attributeName)) {
            attribute = new LineNumberTableAttribute(this);
            attribute.setConstantPool(constantPool);
        }
        else if("LocalVariableTable".equals(attributeName)) {
            attribute = new LocalVariableTableAttribute(this);
            attribute.setConstantPool(constantPool);
        }
        else if("SourceFile".equals(attributeName)) {
            attribute = new SourceFileAttribute(this);
            attribute.setConstantPool(constantPool);
        }
        else if("Synthetic".equals(attributeName)) {
            attribute = new SyntheticAttribute(this);
            attribute.setConstantPool(constantPool);
        } else {
            attribute = new UnparsedAttribute(this, attributeLen);
            attribute.setConstantPool(constantPool);
        }
        return attribute;
    }


//    switch attrName {
//        // case "AnnotationDefault":
//        case "BootstrapMethods":
//            return &BootstrapMethodsAttribute{}
//        case "Code":
//            return &CodeAttribute{cp: cp}
//        case "ConstantValue":
//            return &ConstantValueAttribute{}
//        case "Deprecated":
//            return _attrDeprecated
//        case "EnclosingMethod":
//            return &EnclosingMethodAttribute{cp: cp}
//        case "Exceptions":
//            return &ExceptionsAttribute{}
//        case "InnerClasses":
//            return &InnerClassesAttribute{}
//        case "LineNumberTable":
//            return &LineNumberTableAttribute{}
//        case "LocalVariableTable":
//            return &LocalVariableTableAttribute{}
//        case "LocalVariableTypeTable":
//            return &LocalVariableTypeTableAttribute{}
//        // case "MethodParameters":
//        // case "RuntimeInvisibleAnnotations":
//        // case "RuntimeInvisibleParameterAnnotations":
//        // case "RuntimeInvisibleTypeAnnotations":
//        // case "RuntimeVisibleAnnotations":
//        // case "RuntimeVisibleParameterAnnotations":
//        // case "RuntimeVisibleTypeAnnotations":
//        case "Signature":
//            return &SignatureAttribute{cp: cp}
//        case "SourceFile":
//            return &SourceFileAttribute{cp: cp}
//        // case "SourceDebugExtension":
//        // case "StackMapTable":
//        case "Synthetic":
//            return _attrSynthetic

}
